package com.sk.orm.support;

import com.sk.orm.PageParam;
import com.sk.orm.SortParam;
import com.sk.util.ConvertUtil;
import com.sk.util.LambdaUtil;
import com.sk.util.ObjectUtil;
import jakarta.persistence.Entity;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import java.lang.reflect.*;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * @author smy
 * {@code @date} 2023/12/18
 */
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class Criteria<V> {

    public static final String sql_def_from_as = "e0";

    Class<?> from;

    String fromAs;

    String select;
    Function<Object, V> vf;

    List<String> join = new ArrayList<>();
    List<WhereData> where = new ArrayList<>();

    String groupBy;

    List<WhereData> having = new ArrayList<>();

    List<SortParam> orderBy = new ArrayList<>();

    Integer offset;
    Integer size;


    public static <V> Criteria<V> gen(Class<?> from, String fromAs, String select, Function<Object, V> vf) {
        Criteria<V> criteria = new Criteria<>();
        criteria.select = select;
        criteria.from = from;
        criteria.fromAs = fromAs;
        criteria.vf = vf;
        return criteria;
    }

    @SuppressWarnings("unchecked")
    public static <V> Criteria<V> gen(Class<V> vClass) {
        Entity entity = vClass.getAnnotation(Entity.class);
        if (entity == null) {
            //默认使用第一个带参数的构造器
            Constructor[] constructors = vClass.getConstructors();
            for (Constructor<V> constructor : constructors) {
                if (constructor.getParameterCount() > 0) {
                    Function<Object, V> vf = ConvertUtil.genFactory(constructor);
                    return getByReflect(constructor, vf);
                }
            }
            throw new RuntimeException("没有找到合适的构造器");
        } else {
            //直接查询实体
            return gen(vClass, sql_def_from_as, sql_def_from_as, o -> (V) o);
        }
    }

    private static String selectByParameter(Parameter[] parameters) {
        StringBuilder builder = new StringBuilder();
        Stream.of(parameters).forEach(LambdaUtil.enumerate(((parameter, i) -> {
            Select select = parameter.getAnnotation(Select.class);
            Assert.notNull(select, "%s(%s)缺少Select标签".formatted(parameter.getDeclaringExecutable().toString(), parameter.getName()));
            if (i > 0) {
                builder.append(",");
            }
            builder.append(select.value());
        })));
        return builder.toString();
    }

    public static <V> Criteria<V> genByConstructor(Class<V> vClass, Class<?>... pTypes) {
        return genByConstructor(ObjectUtil.getConstructor(vClass, pTypes));
    }


    public static <V> Criteria<V> genByConstructor(Constructor<V> c) {
        Function<Object, V> vf = ConvertUtil.genFactory(c);
        return getByReflect(c, vf);
    }

    public static <V> Criteria<V> genByMethod(Class<?> v, String methodName) {
        Method method = ObjectUtil.getMethodByName(v, methodName);
        return genByMethod(method);
    }

    public static <V> Criteria<V> genByMethod(Method method) {
        Function<Object, V> vf = ConvertUtil.genFactory(method);
        return getByReflect(method, vf);
    }

    public static <V> Criteria<V> getByReflect(Executable executable, Function<Object, V> vf) {
        Class<?> defFrom = executable.getParameterTypes()[0];
        String select = selectByParameter(executable.getParameters());
        Class<?> from = Optional.ofNullable(executable.getAnnotation(From.class)).map(From::value).orElse(defFrom);
        String fromAs = Optional.ofNullable(executable.getAnnotation(From.class)).map(From::as).orElse(sql_def_from_as);
        Criteria<V> criteria = Criteria.gen(from, fromAs, select, vf);
        criteria.groupBy = Optional.ofNullable(executable.getAnnotation(GroupBy.class)).map(GroupBy::value).orElse(null);
        return criteria.joinByClass(executable.getDeclaringClass()).joinByExecutable(executable);
    }

    public static <V> Criteria<V> eq(Class<V> v, String field, Object data) {
        return gen(v).eq(field, data);
    }

    public static <V> Criteria<V> in(Class<V> v, String field, Collection value) {
        return gen(v).in(field, value);
    }

    public static <V> Criteria<V> whereByTag(Class<V> v, Object query) {
        return gen(v).whereByTag(query);
    }

    public Criteria<V> joinByClass(Class<?> v) {
        for (Join joinTag : v.getAnnotationsByType(Join.class)) {
            this.join.add(joinTag.value());
        }
        return this;
    }

    public Criteria<V> joinByExecutable(Executable executable) {
        for (Join joinTag : executable.getAnnotationsByType(Join.class)) {
            this.join.add(joinTag.value());
        }
        return this;
    }

    public Criteria<V> join(String join) {
        this.join.add(join);
        return this;
    }

    public Criteria<V> whereByTag(Object data) {
        ObjectUtil.getFields(data.getClass()).forEach(f -> {
            Where point = f.getAnnotation(Where.class);
            if (point == null) {
                return;
            }
            f.setAccessible(true);
            Object value = ObjectUtil.getValue(data, f);
            if (value == null) {
                return;
            }
            String[] keys = point.keys();
            WhereData whereData;
            if (keys.length == 1) {
                String name0 = keys[0];
                name0 = "".equals(name0) ? f.getName() : name0;
                whereData = new WhereData(point.value(), name0, value);
                if (point.not()) {
                    whereData.not();
                }
            } else {
                Collection<WhereData> collection = Stream.of(keys).map(key -> new WhereData(point.value(), key, value)).collect(Collectors.toList());
                if (point.not()) {
                    collection.forEach(WhereData::not);
                }
                whereData = new WhereData(WhereType.or, null, collection);
            }
            if (point.having()) {
                having(whereData);
            } else {
                where(whereData);
            }

        });
        return this;
    }


    public Criteria<V> where(WhereData whereData) {
        where.add(whereData);
        return this;
    }

    public Criteria<V> groupBy(String groupBy) {
        this.groupBy = groupBy;
        return this;
    }

    public Criteria<V> having(WhereData whereData) {
        having.add(whereData);
        return this;
    }

    public Criteria<V> orderBy(SortParam... sorts) {
        Arrays.stream(sorts).filter(s -> StringUtils.hasText(s.getSortField())).forEach(orderBy::add);
        return this;
    }

    public Criteria<V> orderByDef(SortParam... sorts) {
        if (orderBy.isEmpty()) {
            orderBy(sorts);
        }
        return this;
    }

    public Criteria<V> asc(String sortField){
        orderBy.add(new SortParam(sortField,"asc"));
        return this;
    }

    public Criteria<V> desc(String sortField){
        orderBy.add(new SortParam(sortField,"desc"));
        return this;
    }

    public Criteria<V> limit(Integer size) {
        return limit(null, size);
    }

    public Criteria<V> limit(Integer offset, Integer size) {
        this.offset = offset;
        this.size = size;
        return this;
    }

    public Criteria<V> page(PageParam page) {
        orderBy(page);
        limit((page.getPageNo() - 1) * page.getPageSize(), page.getPageSize());
        return this;
    }

    public Criteria<V> isNull(String name) {
        this.where.add(new WhereData(WhereType.isNull, name, true));
        return this;
    }

    public Criteria<V> isNotNull(String name) {
        this.where.add(new WhereData(WhereType.isNull, name, true).not());
        return this;
    }

    public Criteria<V> or(Collection<WhereData> value) {
        this.where.add(new WhereData(WhereType.or, null, value));
        return this;
    }

    public Criteria<V> or(WhereData... value) {
        this.where.add(new WhereData(WhereType.or, null, Arrays.asList(value)));
        return this;
    }

    public Criteria<V> and(Collection<WhereData> value) {
        this.where.add(new WhereData(WhereType.and, null, value));
        return this;
    }

    public Criteria<V> and(WhereData... value) {
        this.where.add(new WhereData(WhereType.and, null, Arrays.asList(value)));
        return this;
    }

    public Criteria<V> eq(String name, Object value) {
        this.where.add(new WhereData(WhereType.eq, name, value));
        return this;
    }

    public Criteria<V> not_eq(String name, Object value) {
        this.where.add(new WhereData(WhereType.eq, name, value).not());
        return this;
    }

    public Criteria<V> like(String name, String value) {
        this.where.add(new WhereData(WhereType.like, name, value));
        return this;
    }

    public Criteria<V> not_like(String name, String value) {
        this.where.add(new WhereData(WhereType.like, name, value).not());
        return this;
    }

    public Criteria<V> leftLike(String name, String value) {
        this.where.add(new WhereData(WhereType.leftLike, name, value));
        return this;
    }

    public Criteria<V> rightLike(String name, String value) {
        this.where.add(new WhereData(WhereType.rightLike, name, value));
        return this;
    }

    public Criteria<V> gt(String name, Comparable<?> value) {
        this.where.add(new WhereData(WhereType.gt, name, value));
        return this;
    }

    public Criteria<V> ge(String name, Comparable<?> value) {
        this.where.add(new WhereData(WhereType.ge, name, value));
        return this;
    }

    public Criteria<V> lt(String name, Comparable<?> value) {
        this.where.add(new WhereData(WhereType.lt, name, value));
        return this;
    }

    public Criteria<V> le(String name, Comparable<?> value) {
        this.where.add(new WhereData(WhereType.le, name, value));
        return this;
    }

    public Criteria<V> in(String name, Collection<?> value) {
        this.where.add(new WhereData(WhereType.in, name, value));
        return this;
    }

    public Criteria<V> in(String name, Object[] value) {
        this.where.add(new WhereData(WhereType.in, name, Arrays.asList(value)));
        return this;
    }

    public Criteria<V> not_in(String name, Collection<?> value) {
        this.where.add(new WhereData(WhereType.in, name, value).not());
        return this;
    }

    public Criteria<V> not_in(String name, Object[] value) {
        this.where.add(new WhereData(WhereType.in, name, Arrays.asList(value)).not());
        return this;
    }

    public Criteria<V> query(String query) {
        this.where.add(new WhereData(WhereType.query, query, null));
        return this;
    }

    public Criteria<V> query(String query, Map<String, Object> param) {
        this.where.add(new WhereData(WhereType.query, query, param));
        return this;
    }

}
